import os
from typing import TYPE_CHECKING

from dvc.repo.scm_context import scm_context

from . import locked

if TYPE_CHECKING:
    from . import Repo


def _expand_target_path(from_path, to_path):
    if os.path.isdir(to_path):
        return os.path.join(to_path, os.path.basename(from_path))
    return to_path


@locked
@scm_context
def move(self: "Repo", from_path, to_path):
    """
    Renames an output file and modifies the stage associated
    to reflect the change on the pipeline.

    If the output has the same name as its stage, it would
    also rename the corresponding .dvc file.

    E.g.
          Having: (hello, hello.dvc)

          $ dvc move hello greetings

          Result: (greeting, greeting.dvc)

    It only works with outputs generated by `add` or `import`,
    also known as data sources.
    """
    from dvc import dependency, output
    from dvc.dvcfile import DVC_FILE_SUFFIX
    from dvc.exceptions import MoveNotDataSourceError
    from dvc.stage import Stage
    from dvc.stage.exceptions import StageFileAlreadyExistsError
    from dvc_objects.fs.local import LocalFileSystem

    from_out = output.loads_from(Stage(self), [from_path])[0]
    assert from_out.protocol == "local"

    to_path = _expand_target_path(from_path, to_path)

    outs = self.find_outs_by_path(from_out.fspath)
    assert len(outs) == 1
    out = outs[0]
    stage = out.stage
    deps = stage.deps

    if not stage.is_data_source:
        raise MoveNotDataSourceError(stage.addressing)

    stage_name = os.path.splitext(os.path.basename(stage.path))[0]
    from_name = os.path.basename(from_out.fspath)
    if stage_name == from_name:
        new_fname = os.path.join(
            os.path.dirname(to_path),
            os.path.basename(to_path) + DVC_FILE_SUFFIX,
        )
        new_wdir = os.path.abspath(os.path.join(os.curdir, os.path.dirname(to_path)))
        to_path = os.path.relpath(to_path, new_wdir)
        try:
            new_stage = self.stage.create(
                single_stage=True,
                fname=new_fname,
                wdir=new_wdir,
                outs=[to_path],
                meta=stage.meta,
                frozen=stage.frozen,
                always_changed=stage.always_changed,
                desc=stage.desc,
            )
        except StageFileAlreadyExistsError:
            # reraise to remove `--force` hint
            raise StageFileAlreadyExistsError(f"{new_fname!r} already exists") from None
    else:
        new_stage = stage
        to_path = os.path.relpath(to_path, stage.wdir)

    def with_dep_path_adjusted(dep: dependency.Dependency):
        d = dep.dumpd()
        if isinstance(dep.fs, LocalFileSystem) and not os.path.isabs(dep.def_path):
            return d | {"path": os.path.relpath(dep.fspath, new_stage.wdir)}
        return d

    new_stage.outs = output.loadd_from(new_stage, [out.dumpd() | {"path": to_path}])
    new_stage.deps = dependency.loadd_from(
        new_stage, [with_dep_path_adjusted(dep) for dep in deps]
    )
    out.move(new_stage.outs[0])
    new_stage.md5 = new_stage.compute_md5()
    new_stage.dump()
    if stage != new_stage:
        stage.dvcfile.remove()
        self.scm_context.track_file(stage.dvcfile.relpath)
    return stage, new_stage
